/*
 * ========THE SOLMIX PROJECT=====================================
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.gnu.org/licenses/ 
 * or see the FSF site: http://www.fsf.org. 
 */
package org.solmix.commons.util;


/**
 * 
 * @author solomon
 * @version $Id$  2011-3-15
 */

import java.util.ArrayList;
import java.util.List;

public class SimpleFilter
{

   public static final int AND = 1;

   public static final int OR = 2;

   public static final int NOT = 3;

   public static final int EQ = 4;

   public static final int LTE = 5;

   public static final int GTE = 6;

   public static final int SUBSTRING = 7;

   public static final int PRESENT = 8;

   public static final int APPROX = 9;

   private final String m_name;

   private final Object m_value;

   private final int m_op;

   public SimpleFilter(String attr, Object value, int op)
   {
      m_name = attr;
      m_value = value;
      m_op = op;
   }

   public String getName()
   {
      return m_name;
   }

   public Object getValue()
   {
      return m_value;
   }

   public int getOperation()
   {
      return m_op;
   }

   public String toString()
   {
      String s = null;
      switch (m_op)
      {
         case AND:
            s = "(&" + toString((List) m_value) + ")";
            break;
         case OR:
            s = "(|" + toString((List) m_value) + ")";
            break;
         case NOT:
            s = "(!" + toString((List) m_value) + ")";
            break;
         case EQ:
            s = "(" + m_name + "=" + toEncodedString(m_value) + ")";
            break;
         case LTE:
            s = "(" + m_name + "<=" + toEncodedString(m_value) + ")";
            break;
         case GTE:
            s = "(" + m_name + ">=" + toEncodedString(m_value) + ")";
            break;
         case SUBSTRING:
            s = "(" + m_name + "=" + unparseSubstring((List<String>) m_value) + ")";
            break;
         case PRESENT:
            s = "(" + m_name + "=*)";
            break;
         case APPROX:
            s = "(" + m_name + "~=" + toEncodedString(m_value) + ")";
            break;
      }
      return s;
   }

   private static String toString(List list)
   {
      StringBuffer sb = new StringBuffer();
      for (int i = 0; i < list.size(); i++)
      {
         sb.append(list.get(i).toString());
      }
      return sb.toString();
   }

   private static String toDecodedString(String s, int startIdx, int endIdx)
   {
      StringBuffer sb = new StringBuffer(endIdx - startIdx);
      boolean escaped = false;
      for (int i = 0; i < (endIdx - startIdx); i++)
      {
         char c = s.charAt(startIdx + i);
         if (!escaped && (c == '\\'))
         {
            escaped = true;
         } else
         {
            escaped = false;
            sb.append(c);
         }
      }

      return sb.toString();
   }

   private static String toEncodedString(Object o)
   {
      if (o instanceof String)
      {
         String s = (String) o;
         StringBuffer sb = new StringBuffer();
         for (int i = 0; i < s.length(); i++)
         {
            char c = s.charAt(i);
            if ((c == '\\') || (c == '(') || (c == ')') || (c == '*'))
            {
               sb.append('\\');
            }
            sb.append(c);
         }

         o = sb.toString();
      }

      return o.toString();
   }

   public static SimpleFilter parse(String filter)
   {
      int idx = skipWhitespace(filter, 0);

      if ((filter == null) || (filter.length() == 0) || (idx >= filter.length()))
      {
         throw new IllegalArgumentException("Null or empty filter.");
      } else if (filter.charAt(idx) != '(')
      {
         throw new IllegalArgumentException("Missing opening parenthesis: " + filter);
      }

      SimpleFilter sf = null;
      List stack = new ArrayList();
      boolean isEscaped = false;
      while (idx < filter.length())
      {
         if (sf != null)
         {
            throw new IllegalArgumentException("Only one top-level operation allowed: " + filter);
         }

         if (!isEscaped && (filter.charAt(idx) == '('))
         {
            // Skip paren and following whitespace.
            idx = skipWhitespace(filter, idx + 1);

            if (filter.charAt(idx) == '&')
            {
               int peek = skipWhitespace(filter, idx + 1);
               if (filter.charAt(peek) == '(')
               {
                  idx = peek - 1;
                  stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.AND));
               } else
               {
                  stack.add(0, new Integer(idx));
               }
            } else if (filter.charAt(idx) == '|')
            {
               int peek = skipWhitespace(filter, idx + 1);
               if (filter.charAt(peek) == '(')
               {
                  idx = peek - 1;
                  stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.OR));
               } else
               {
                  stack.add(0, new Integer(idx));
               }
            } else if (filter.charAt(idx) == '!')
            {
               int peek = skipWhitespace(filter, idx + 1);
               if (filter.charAt(peek) == '(')
               {
                  idx = peek - 1;
                  stack.add(0, new SimpleFilter(null, new ArrayList(), SimpleFilter.NOT));
               } else
               {
                  stack.add(0, new Integer(idx));
               }
            } else
            {
               stack.add(0, new Integer(idx));
            }
         } else if (!isEscaped && (filter.charAt(idx) == ')'))
         {
            Object top = stack.remove(0);
            if (top instanceof SimpleFilter)
            {
               if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter))
               {
                  ((List) ((SimpleFilter) stack.get(0)).m_value).add(top);
               } else
               {
                  sf = (SimpleFilter) top;
               }
            } else if (!stack.isEmpty() && (stack.get(0) instanceof SimpleFilter))
            {
               ((List) ((SimpleFilter) stack.get(0)).m_value).add(SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx));
            } else
            {
               sf = SimpleFilter.subfilter(filter, ((Integer) top).intValue(), idx);
            }
         } else if (!isEscaped && (filter.charAt(idx) == '\\'))
         {
            isEscaped = true;
         } else
         {
            isEscaped = false;
         }

         idx = skipWhitespace(filter, idx + 1);
      }

      if (sf == null)
      {
         throw new IllegalArgumentException("Missing closing parenthesis: " + filter);
      }

      return sf;
   }

   private static SimpleFilter subfilter(String filter, int startIdx, int endIdx)
   {
      final String opChars = "=<>~";

      // Determine the ending index of the attribute name.
      int attrEndIdx = startIdx;
      for (int i = 0; i < (endIdx - startIdx); i++)
      {
         char c = filter.charAt(startIdx + i);
         if (opChars.indexOf(c) >= 0)
         {
            break;
         } else if (!Character.isWhitespace(c))
         {
            attrEndIdx = startIdx + i + 1;
         }
      }
      if (attrEndIdx == startIdx)
      {
         throw new IllegalArgumentException("Missing attribute name: " + filter.substring(startIdx, endIdx));
      }
      String attr = filter.substring(startIdx, attrEndIdx);

      // Skip the attribute name and any following whitespace.
      startIdx = skipWhitespace(filter, attrEndIdx);

      // Determine the operator type.
      int op = -1;
      switch (filter.charAt(startIdx))
      {
         case '=':
            op = EQ;
            startIdx++;
            break;
         case '<':
            if (filter.charAt(startIdx + 1) != '=')
            {
               throw new IllegalArgumentException("Unknown operator: " + filter.substring(startIdx, endIdx));
            }
            op = LTE;
            startIdx += 2;
            break;
         case '>':
            if (filter.charAt(startIdx + 1) != '=')
            {
               throw new IllegalArgumentException("Unknown operator: " + filter.substring(startIdx, endIdx));
            }
            op = GTE;
            startIdx += 2;
            break;
         case '~':
            if (filter.charAt(startIdx + 1) != '=')
            {
               throw new IllegalArgumentException("Unknown operator: " + filter.substring(startIdx, endIdx));
            }
            op = APPROX;
            startIdx += 2;
            break;
         default:
            throw new IllegalArgumentException("Unknown operator: " + filter.substring(startIdx, endIdx));
      }

      // Parse value.
      Object value = toDecodedString(filter, startIdx, endIdx);

      // Check if the equality comparison is actually a substring
      // or present operation.
      if (op == EQ)
      {
         String valueStr = filter.substring(startIdx, endIdx);
         List<String> values = parseSubstring(valueStr);
         if ((values.size() == 2) && (values.get(0).length() == 0) && (values.get(1).length() == 0))
         {
            op = PRESENT;
         } else if (values.size() > 1)
         {
            op = SUBSTRING;
            value = values;
         }
      }

      return new SimpleFilter(attr, value, op);
   }

   public static List<String> parseSubstring(String value)
   {
      List<String> pieces = new ArrayList<String>();
      StringBuffer ss = new StringBuffer();
      // int kind = SIMPLE; // assume until proven otherwise
      boolean wasStar = false; // indicates last piece was a star
      boolean leftstar = false; // track if the initial piece is a star
      boolean rightstar = false; // track if the final piece is a star

      int idx = 0;

      // We assume (sub)strings can contain leading and trailing blanks
      boolean escaped = false;
      loop: for (;;)
      {
         if (idx >= value.length())
         {
            if (wasStar)
            {
               // insert last piece as "" to handle trailing star
               rightstar = true;
            } else
            {
               pieces.add(ss.toString());
               // accumulate the last piece
               // note that in the case of
               // (cn=); this might be
               // the string "" (!=null)
            }
            ss.setLength(0);
            break loop;
         }

         // Read the next character and account for escapes.
         char c = value.charAt(idx++);
         if (!escaped && ((c == '(') || (c == ')')))
         {
            throw new IllegalArgumentException("Illegal value: " + value);
         } else if (!escaped && (c == '*'))
         {
            if (wasStar)
            {
               // encountered two successive stars;
               // I assume this is illegal
               throw new IllegalArgumentException("Invalid filter string: " + value);
            }
            if (ss.length() > 0)
            {
               pieces.add(ss.toString()); // accumulate the pieces
               // between '*' occurrences
            }
            ss.setLength(0);
            // if this is a leading star, then track it
            if (pieces.size() == 0)
            {
               leftstar = true;
            }
            wasStar = true;
         } else if (!escaped && (c == '\\'))
         {
            escaped = true;
         } else
         {
            escaped = false;
            wasStar = false;
            ss.append(c);
         }
      }
      if (leftstar || rightstar || pieces.size() > 1)
      {
         // insert leading and/or trailing "" to anchor ends
         if (rightstar)
         {
            pieces.add("");
         }
         if (leftstar)
         {
            pieces.add(0, "");
         }
      }
      return pieces;
   }

   public static String unparseSubstring(List<String> pieces)
   {
      StringBuffer sb = new StringBuffer();
      for (int i = 0; i < pieces.size(); i++)
      {
         if (i > 0)
         {
            sb.append("*");
         }
         sb.append(toEncodedString(pieces.get(i)));
      }
      return sb.toString();
   }

   public static boolean compareSubstring(List<String> pieces, String s)
   {
      // Walk the pieces to match the string
      // There are implicit stars between each piece,
      // and the first and last pieces might be "" to anchor the match.
      // assert (pieces.length > 1)
      // minimal case is <string>*<string>

      boolean result = true;
      int len = pieces.size();

      // Special case, if there is only one piece, then
      // we must perform an equality test.
      if (len == 1)
      {
         return s.equals(pieces.get(0));
      }

        // Otherwise, check whether the pieces match
      // the specified string.

      int index = 0;

      loop: for (int i = 0; i < len; i++)
      {
         String piece = pieces.get(i);

         // If this is the first piece, then make sure the
         // string starts with it.
         if (i == 0)
         {
            if (!s.startsWith(piece))
            {
               result = false;
               break loop;
            }
         }

         // If this is the last piece, then make sure the
         // string ends with it.
         if (i == len - 1)
         {
            if (s.endsWith(piece))
            {
               result = true;
            } else
            {
               result = false;
            }
            break loop;
         }

         // If this is neither the first or last piece, then
         // make sure the string contains it.
         if ((i > 0) && (i < (len - 1)))
         {
            index = s.indexOf(piece, index);
            if (index < 0)
            {
               result = false;
               break loop;
            }
         }

         // Move string index beyond the matching piece.
         index += piece.length();
      }

      return result;
   }

   private static int skipWhitespace(String s, int startIdx)
   {
      int len = s.length();
      while ((startIdx < len) && Character.isWhitespace(s.charAt(startIdx)))
      {
         startIdx++;
      }
      return startIdx;
   }
}
